﻿/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Xml.Linq;
using QUT.Bio.SilverMap.MapService;
using QUT.Bio.SilverMap.ObjectModel;
using QUT.Bio.Util;

namespace QUT.Bio.SilverMap {
	/// <summary>
	/// ViewModel extracts data from a collection of blast hits and mangles into a form suitable 
	/// for use in the Map application.
	/// <list>
	/// <item>Data is supplied by an</item>
	/// <item>Alternatively, we can populate the model from a StreamReader connected to an XML document.</item>
	/// <item>As a third choice, we can construct and insert collections directly.</item>
	/// </list>
	/// </summary>
	
	public class ViewModel {
		private ILinearDomain referenceSequence;
		private DistanceType distanceType = DistanceType.EValue;
		private double cutoff = 0.5;
		private DataProvider dataProvider;

		private readonly Dictionary<ILinearDomain,SequenceSummary> sequenceSummaries = new Dictionary<ILinearDomain,SequenceSummary>();
		private readonly SparseMatrix<ILinearDomain,ILinearDomain,double> distances = new SparseMatrix<ILinearDomain,ILinearDomain,double>();

		private Stretcher stretcher;
		private Normalizer normalizer;

		public event Action ReferenceSequenceUpdated;
		public event Action DataCollectionLoaded;
		public event Action DataCollectionModified;
		public event Action HistoryUpdated;
		
		private readonly List<HistoryItem> history = new List<HistoryItem>();
		
		private ReadonlyIndexer<ILinearDomain, SequenceSummary> sequenceSummaryIndexer;

		#region Constructor
		/// <summary>
		/// Initialises this ViewModel, setting
		/// </summary>

		public ViewModel () {
			stretcher = new Stretcher();
			normalizer = new LogarithmicNormalizer( 1e-300, 10, stretcher );
			sequenceSummaryIndexer = new DictionaryBackedIndexer<ILinearDomain, SequenceSummary>( sequenceSummaries );
		}
		#endregion

		#region Property: ReferenceSequence
		/// <summary>
		/// Get or set the reference node. When a new value is set, triggers the ReferenceSequenceUpdated event.
		/// </summary>

		public ILinearDomain ReferenceSequence {
			get {
				return referenceSequence;
			}
			set {
				if ( referenceSequence != value ) {
					referenceSequence = value;

					if ( referenceSequence != null ) {
						HistoryItem historyItem = new HistoryItem( referenceSequence );
						historyItem.Time = DateTime.Now;
						history.Add( historyItem );
						
						if ( null != HistoryUpdated ) {
							HistoryUpdated();
						}
					}
					
					if ( null != ReferenceSequenceUpdated ) {
						ReferenceSequenceUpdated();
					}
				}
			}
		} 
		#endregion

		#region Property: DistanceType
		/// <summary>
		/// Get or set the Blast distance type.
		/// </summary>
		public DistanceType DistanceType {
			get {
				return distanceType;
			}
			set {
				distanceType = value;
			}
		} 
		#endregion
		
		#region Property: Cutoff
		/// <summary>
		/// Gets the present cutoff value. This is set as a side effect of Load().
		/// </summary>
		
		public double Cutoff {
			get {
				return cutoff;
			}
			set {
				cutoff = value;
			}
		}
		#endregion

		#region Property: SequenceSummaries
		/// <summary>
		///		Gets the list of sequence summaries presently loaded in to the map.
		///		Summary objects are introduced to provide a data-bindable intermediate layer 
		///		that supports the notion of sequence selection.
		///		<para>The full object model is available via DataProvider.ObjectModel.</para>
		/// </summary>

		public IEnumerable<SequenceSummary> SequenceSummaries {
			get {
				return sequenceSummaries.Values;
			}
		}
		#endregion
		
		#region Indexer: SequenceSummary
		/// <summary>
		/// Gets an indexer that provides the sequenceSummary for each ISequence in this collection.
		/// </summary>
		
		public ReadonlyIndexer<ILinearDomain,SequenceSummary> SequenceSummary {
			get {
				return sequenceSummaryIndexer;
			}
		}
		#endregion

		#region Property: SelectedSequences (not implemented as yet)
		/// <summary>
		/// Get or set a list containing the selected sequences.
		/// A sequence is selected if there is at least one selected feature
		/// that references the sequence.
		/// </summary>
		public IEnumerable<ILinearDomain> SelectedSequences {
			get {
				return from summary in sequenceSummaries.Values
					   where summary.Selected
					   select summary.Sequence;
			}
			set {
				foreach ( SequenceSummary summary in sequenceSummaries.Values ) {
					summary.Selected = false;
				}
				
				foreach ( ILinearDomain seq in value ) {
					sequenceSummaries[seq].Selected = true;
				}
			}
		}
		#endregion

		#region Property: SelectedSequenceSummaries
		/// <summary>
		/// Gets a list containing the summaries of all selected hits.
		/// </summary>

		public IEnumerable<SequenceSummary> SelectedSequenceSummaries {
			get {
				return from summary in sequenceSummaries.Values
					   where summary.Selected
					   select summary;
			}
		}
		#endregion

		#region Property: DataProvider
		/// <summary>
		/// Get or set the current data producer attached to the map.
		/// <para>When a producer is registered the following things happen:</para>
		/// <list>
		/// <item>Detach from load complete of previously registered producer.</item>
		/// <item>Begin listening for load-complete on new prducer.</item>
		/// <item>Rebuild the model, on the assumption that a load has probably already been executed.</item>
		/// </list>
		/// </summary>

		public DataProvider DataProducer {
			get {
				return dataProvider;
			}
			set {
				if ( dataProvider != null ) {
					dataProvider.LoadComplete -= dataProducer_LoadComplete;
					dataProvider.CollectionModified -= dataProducer_CollectionModified;
				}

				dataProvider = value;
				dataProvider.LoadComplete += dataProducer_LoadComplete;
				dataProvider.CollectionModified += dataProducer_CollectionModified;
			}
		}
		#endregion

		#region Property: Normalizer
		/// <summary>
		/// Get or set a reference to the normalizer in use by this model.
		/// <para>Contract for setter:</para>
		/// <para>Pre: value not null</para>
		/// </summary>
		/// <exception cref="ArgumentException">If value is null.</exception>

		public Normalizer Normalizer {
			get {
				return normalizer;
			}
			set {
				if ( value == null ) {
					throw new ArgumentException( "Value nay not be null." );
				}

				normalizer = value;
			}
		}
		#endregion

		#region Property: Stretcher
		/// <summary>
		///	Gets a reference to the stretch transformation for this map. 
		/// </summary>
		public Stretcher Stretcher {
			get {
				return stretcher;
			}
		}
		#endregion
		
		#region Property: History
		/// <summary>
		/// Gets a reference to the list of reference sequences that have been used in this session.
		/// </summary>
		
		public IEnumerable<HistoryItem> History {
			get {
				return history;
			}
			set {
				history.Clear();
				
				foreach ( HistoryItem item in value ) {
					history.Add( item );
				}
				
				if ( HistoryUpdated != null ) {
					HistoryUpdated();
				}
			}
		}
		#endregion
		
		#region Private method: dataProducer_CollectionModified
		
		/// <summary>
		/// Updates the sequence summary and distance tables in a more surgical manner.
		/// </summary>
		/// <param name="producer"></param>
		
		private void dataProducer_CollectionModified ( DataProvider producer ) {
			UpdateSequences();
			
			if ( DataCollectionModified != null ) {
				DataCollectionModified();
			}
		}
		#endregion

		#region Private method: dataProducer_LoadComplete
		/// <summary>
		/// Repopulates the distance, hit and feature tables in response to a
		/// LoadComplete event from a data provider and notifies any 
		/// DataCollectionLoaded data
		/// </summary>
		/// <param name="source">The source of the LoadComplete event.</param>

		private void dataProducer_LoadComplete ( DataProvider source ) {
			ReplaceSequences();

			if ( DataCollectionLoaded != null ) {
				DataCollectionLoaded();
			}
		}
		#endregion

		#region Private method: ReplaceSequences
		/// <summary>
		/// Replaces the contents of the hits and distances tables.
		/// </summary>

		private void ReplaceSequences () {
			sequenceSummaries.Clear();
			distances.Clear();

			foreach ( ILinearDomain seq in dataProvider.FeatureCollection.Domains ) {
				sequenceSummaries.Add( seq, new SequenceSummary( this, seq ) );
			}

			foreach ( Hit hit in dataProvider.FeatureCollection.Hits ) {
				ILinearDomain querySequence = hit.QueryFeature.ContainingSequence;
				ILinearDomain hitSequence = hit.HitFeature.ContainingSequence;
				double d = Distance( hit );
				
				if ( distances.Contains( querySequence, hitSequence ) ) {
					if ( d < distances[querySequence, hitSequence] ) {
						distances[querySequence, hitSequence] = d;
					}
				}
				else {
					distances[querySequence, hitSequence] = d;
				}
			}
			
			ReferenceSequence = dataProvider.FeatureCollection.InitialFocus;
		}
		#endregion

		#region Method: UpdateSequences
		/// <summary>
		/// Similar to ReplaceSequences, but intended for incremental change,
		/// that is, when we do not want to burn down the entire display and create a new one,
		/// but instead we want to keep the display, but update it.
		/// </summary>

		private void UpdateSequences () {
			HashList<ILinearDomain> sequencesToRemove = new HashList<ILinearDomain>();
			sequencesToRemove.AddRange( sequenceSummaries.Keys );

			foreach ( ILinearDomain seq in dataProvider.FeatureCollection.Domains ) {
				sequencesToRemove.Remove( seq );
			}

			foreach ( ILinearDomain seq in sequencesToRemove ) {
				sequenceSummaries.Remove( seq );
			}

			foreach ( ILinearDomain seq in dataProvider.FeatureCollection.Domains ) {
				if ( !sequenceSummaries.ContainsKey( seq ) ) {
					sequenceSummaries.Add( seq, new SequenceSummary( this, seq ) );
				}
			}

			SparseMatrix<ILinearDomain, ILinearDomain, Hit> indexedHits = new SparseMatrix<ILinearDomain, ILinearDomain, Hit>();
			List<Hit> hitsToAdd = new List<Hit>();

			foreach ( Hit hit in dataProvider.FeatureCollection.Hits ) {
				ILinearDomain querySequence = hit.QueryFeature.ContainingSequence;
				ILinearDomain hitSequence = hit.HitFeature.ContainingSequence;

				if ( !indexedHits.Contains( querySequence, hitSequence ) ) {
					indexedHits.Add( querySequence, hitSequence, hit, false );
				}

				if ( !distances.Contains( querySequence, hitSequence ) ) {
					hitsToAdd.Add( hit );
				}
			}

			foreach ( Triple<ILinearDomain, ILinearDomain, double> triple in distances ) {
				indexedHits.Remove( triple.First, triple.Second );
			}

			foreach ( Triple<ILinearDomain, ILinearDomain, Hit> triple in indexedHits ) {
				distances.Remove( triple.First, triple.Second );
			}

			ILinearDomain anySequence = null;
			
			foreach ( Hit hit in hitsToAdd ) {
				ILinearDomain querySequence = hit.QueryFeature.ContainingSequence;
				ILinearDomain hitSequence = hit.HitFeature.ContainingSequence;
				double d = Distance( hit );
				
				if ( anySequence == null ) {
					anySequence = querySequence;
				}

				if ( distances.Contains( querySequence, hitSequence ) ) {
					if ( d < distances[querySequence, hitSequence] ) {
						distances[querySequence, hitSequence] = d;
					}
				}
				else {
					distances[querySequence, hitSequence] = d;
				}
			}
			
			if ( referenceSequence == null ) {
				ReferenceSequence = anySequence;
			}
		} 
		#endregion

#if TAGS_ENABLED
		
		public bool TagsEnabled {
			get {
				return tagsEnabled;
			}
			set {
				if ( value != tagsEnabled ) {
					tagsEnabled = value;

					if ( value ) {
						InitializeTags();
					}
				}
			}
		}
		private void InitializeTags () {
			Tags.ObjectNamespaces[typeof( Gene )] = "mquter_map_gene";
			Tags.ObjectNamespaces[typeof( Chromosome )] = "mquter_map_chromosome";
			Tags.ObjectNamespaces[typeof( DistanceRow )] = "mquter_map_hit";

			Tags.GetObjectHavingId = delegate( Type type, string id ) {
				if ( type.Equals( typeof( Gene ) ) ) {
					return GetGene( decimal.Parse( id ) );
				}
				else if ( type.Equals( typeof( Chromosome ) ) ) {
					return GetChromosome( id );
				}
				else if ( type.Equals( typeof( DistanceRow ) ) ) {
					string[] ids = id.Split( '_' );
					return distances.Contains( decimal.Parse( ids[0] ), decimal.Parse( ids[1] ) );
				}
				else {
					return null;
				}
			};

			Tags.GetIdOfObject = delegate( object x ) {
				if ( x is Gene ) {
					return ( x as Gene ).Id.ToString();
				}

				if ( x is Chromosome ) {
					return ( x as Chromosome ).Accession.ToString();
				}

				if ( x is DistanceRow ) {
					return string.Format( "{0}_{1}", ( x as DistanceRow ).QueryId, ( x as DistanceRow ).HitId );
				}

				return null;
			};

			Tags.WriteLine = Status.WriteLine;
			Tags.Initialize();
		}
		
#endif

#if LOAD_SAVE_ENABLED
		/// <summary>
		/// Populate this model with the contents of an XML document.
		/// [http://www.mquter.qut.edu.au/SilverMap/DocFormat.html]
		/// </summary>
		/// <param name="reader">A reader attached to an input stream containing content for the model.</param>
		public void Load ( StreamReader reader ) {
			try {
				XDocument xdoc = XDocument.Load( reader );
				XElement mapElement = xdoc.Element( "Map" );
				var organismList = mapElement.Element( "Organisms" ).Elements( "Organism" );
				var geneList = mapElement.Element( "Genes" ).Elements( "Gene" );
				var hitList = mapElement.Element( "Hits" ).Elements( "Hit" );

				var organisms = from organismElement in organismList
								select new Chromosome( new MapService.Organism {
									Accession = organismElement.Element( "Accession" ).Value,
									Description = organismElement.Element( "Description" ).Value,
									GeneCount = int.Parse( organismElement.Element( "GeneCount" ).Value )
								} );

				Chromosomes = organisms.ToArray();

				List<Gene> genes = new List<Gene>();

				foreach ( XElement geneElement in xdoc.Element( "Map" ).Element( "Genes" ).Elements( "Gene" ) ) {
					Gene gene = new Gene( this,
						decimal.Parse( geneElement.Element( "Id" ).Value ),
						geneElement.Element( "Accession" ).Value,
						geneElement.Element( "LocusTag" ).Value,
						geneElement.Element( "Symbol" ).Value,
						geneElement.Element( "Product" ).Value,
						geneElement.Element( "GI" ).Value,
						geneElement.Element( "Cog" ).Value
					);

					XElement childElement = null;

					if ( null != ( childElement = geneElement.Element( "Note" ) ) ) {
						gene.Note = childElement.Value;
					}

					if ( null != ( childElement = geneElement.Element( "ProteinId" ) ) ) {
						gene.ProteinId = childElement.Value;
					}

					if ( null != ( childElement = geneElement.Element( "Translation" ) ) ) {
						gene.Translation = childElement.Value;
					}

					genes.Add( gene );
				}

				Genes = genes.ToArray();

				var hitElements = xdoc.Element( "Map" ).Element( "Hits" ).Elements( "Hit" );

				List<Triple<decimal, decimal, double>> distances = new List<Triple<decimal, decimal, double>>();

				List<Triple<decimal, decimal, BlastDetails>> newDetails = 
					new List<Triple<decimal, decimal, BlastDetails>>();

				foreach ( XElement distanceElement in hitElements ) {
					DistanceRow row = new DistanceRow(
						decimal.Parse( distanceElement.Element( "QueryId" ).Value ),
						decimal.Parse( distanceElement.Element( "HitId" ).Value ),
						double.Parse( distanceElement.Element( "Distance" ).Value )
					);

					BlastDetails blastDetails = new BlastDetails();

					ObjectWrapper bd = new ObjectWrapper( blastDetails );

					foreach ( XElement propertyElement in distanceElement.Elements() ) {
						string name = propertyElement.Name.LocalName;

						if ( name != "QueryId" && name != "HitId" && name != "Distance" ) {
							bd[name] = propertyElement.Value;
						}
					}

					distances.Add( row );
				}

				this.distances.AddRange( distances );
				this.blastDetails.AddRange( newDetails );

				decimal referenceGeneId = 0;

				try {
					referenceGeneId = decimal.Parse( xdoc.Element( "Map" ).Element( "InitialReferenceGene" ).Value );
					ReferenceGene = GetGene( referenceGeneId );
				}
				catch {
				}

				try {
					string [] selectedGeneIdStrings = xdoc.Element( "Map" ).Element( "SelectedGeneIds" ).Value.Split( ';' );
					SelectedGeneIds = ( from s in selectedGeneIdStrings
										select decimal.Parse( s ) ).ToArray();
				}
				catch {
				}

				Status.WriteLine( "Reference gene: {0}\n\nGenes:\n{1}\n\nDistances:\n{2}",
					ReferenceGene,
					genes.Join( "\n" ),
					distances.Join( "\n" )
				);

				if ( GenesAndDistancesLoaded != null ) {
					GenesAndDistancesLoaded();
				}
			}
			catch ( Exception e ) {
				Status.WriteLine( "Exception caught: {0}", e );
			}
		}

		/// <summary>
		/// SAves the contents of the model to a xml document.
		/// </summary>
		/// <param name="writer"></param>
		public void Save ( StreamWriter writer ) {
			XDocument xdoc = new XDocument();
			XElement map = new XElement( "Map" );
			XElement orgs = new XElement( "Organisms" );
			XElement genes = new XElement( "Genes" );
			XElement hits = new XElement( "Hits" );

			xdoc.Add( map );
			map.Add( orgs, genes, hits );

			foreach ( Chromosome org in this.Chromosomes ) {
				XElement orgElement = new XElement( "Organism",
					new XElement( "Accession", org.Accession ),
					new XElement( "Description", org.Description ),
					new XElement( "GeneCount", org.GeneCount )
				);

				orgs.Add( orgElement );
			}

			foreach ( Gene g in this.genes ) {
				XElement geneElement = new XElement( "Gene",
					new XElement( "Id", g.Id ),
					new XElement( "Accession", g.Accession ),
					new XElement( "LocusTag", g.LocusTag ),
					new XElement( "Symbol", g.Symbol ),
					new XElement( "Product", g.Product ),
					new XElement( "GI", g.GI ),
					new XElement( "Cog", g.Cog )
				);

				if ( g.Note != null ) {
					geneElement.Add( new XElement( "Note", g.Note ) );
				}

				if ( g.Translation != null ) {
					geneElement.Add( new XElement( "Translation", g.Translation ) );
				}

				if ( g.ProteinId != null ) {
					geneElement.Add( new XElement( "ProteinId", g.ProteinId ) );
				}

				genes.Add( geneElement );
			}

			foreach ( DistanceRow r in distances ) {
				XElement hitElement = new XElement( "Hit",
					new XElement( "QueryId", r.QueryId ),
					new XElement( "HitId", r.HitId ),
					new XElement( "Distance", r.Distance )
				);

				if ( blastDetails.Contains( r.QueryId, r.HitId ) ) {
					BlastDetails bd = blastDetails[r.QueryId, r.HitId];

					hitElement.Add(
						new XElement( "EValue", bd.EValue ),
						new XElement( "HitFrom", bd.HitFrom ),
						new XElement( "HitSequence", bd.HitSequence ),
						new XElement( "HitTo", bd.HitTo ),
						new XElement( "Identities", bd.Identities ),
						new XElement( "Midline", bd.Midline ),
						new XElement( "Positives", bd.Positives ),
						new XElement( "QueryFrom", bd.QueryFrom ),
						new XElement( "QuerySequence", bd.QuerySequence ),
						new XElement( "QueryTo", bd.QueryTo )
					);
				}

				hits.Add( hitElement );
			}

			if ( referenceGene != null ) {
				map.Add( new XElement( "InitialReferenceGene", referenceGene.Id ) );
			}

			decimal [] selectedGeneIds = SelectedGeneIds;

			if ( selectedGeneIds != null ) {
				map.Add( new XElement( "SelectedGeneIds", selectedGeneIds.Join( ";" ) ) );
			}

			xdoc.Save( writer );
		} 
#endif

		#region Method: Distance( Hit hit )
		/// <summary>
		/// Gets the "distance" represented by a hit, as determined by the present DistanceType setting.
		/// </summary>
		/// <param name="hit">A hit for which we wish to find the distance.</param>
		/// <returns></returns>

		public double Distance ( Hit hit ) {
			return distanceType == DistanceType.EValue ? hit.EValue
			: distanceType == DistanceType.Identities ? (1.0 - hit.Identities) / hit.QueryFeature.ContainingSequence.Length
			: ( 1.0 - hit.Positives ) / hit.QueryFeature.ContainingSequence.Length;
		}
		#endregion

		#region Method: DistanceFromReferenceSequence
		/// <summary>
		/// Gets the distance of a nominated sequence from the current reference sequence.
		/// </summary>
		/// <param name="sequence">The sequence for which we want to find the distance.</param>
		/// <returns></returns>

		public double DistanceFromReferenceSequence ( ILinearDomain sequence ) {
			if ( ReferenceSequence == null ) {
				return double.PositiveInfinity;
			}

			if ( sequence.Equals( ReferenceSequence ) ) {
				return 0.0;
			}

			if ( distances.Contains( referenceSequence, sequence ) ) {
				return distances[referenceSequence, sequence];
			}

			return double.PositiveInfinity;
		}
		#endregion

		#region Method: NormalizedDistanceFromReferenceSequence
		/// <summary>
		/// Gets the distance of a nominated sequence from the current reference sequence.
		/// </summary>
		/// <param name="sequence">The sequence for which we want to find the distance.</param>
		/// <returns></returns>

		public double NormalizedDistanceFromReferenceSequence ( ILinearDomain sequence ) {
			if ( ReferenceSequence == null ) {
				return 1.0;
			}

			if ( sequence.Equals( ReferenceSequence ) ) {
				return 0.0;
			}

			if ( distances.Contains( referenceSequence, sequence ) ) {
				return normalizer.Normalize( distances[referenceSequence, sequence] );
			}

			return 1.0;
		}
		#endregion
	}
}
